
import random
import string

alphabet = list(string.ascii_uppercase)

def generate_derangement():
    while True:
        shuffled = alphabet[:]
        random.shuffle(shuffled)
        if all(a != b for a, b in zip(alphabet, shuffled)):  # Ensure derangement
            return dict(zip(alphabet, shuffled))

def encrypt_quote(quote, key):
    quote = quote.upper()
    return ''.join(key.get(char, char) for char in quote)  # Substitute only letters

def new_puzzle(data_manager, quote_db):
    quotes = data_manager.read_quote_db(quote_db)
    quote = random.choice(quotes)
    key = generate_derangement()
    return Puzzle(quote, key)

class Puzzle:
    def __init__(self, quote, key, letter_mappings = {}):
        self.quote = quote
        self.key = key
        self.decrypted_text = encrypt_quote(quote['quote'], dict(zip(alphabet, alphabet)))
        self.encrypted_text = encrypt_quote(quote['quote'], key)
        self.letter_mappings = letter_mappings.copy()
        self.reset_cursor_position()

    def reset_cursor_position(self):
        self.cursor_position = 0
        while not self.encrypted_text[self.cursor_position].isalpha():
            if self.cursor_position + 1 >= len(self.encrypted_text):
                self.cursor_position = 0
            else:
                self.cursor_position += 1

    def jump_cursor(self, new_index):
        self.cursor_position = new_index

    def shift_cursor(self, direction):
        if direction == -1:
            # Move left until an alphabetic character is found
            if self.cursor_position - 1 < 0:
                self.cursor_position = len(self.encrypted_text) - 1
            else:
                self.cursor_position -= 1
            while not self.encrypted_text[self.cursor_position].isalpha():
                if self.cursor_position - 1 < 0:
                    self.cursor_position = len(self.encrypted_text) - 1
                else:
                    self.cursor_position -= 1
        elif direction == 1:
            # Move right until an alphabetic character is found
            if self.cursor_position + 1 >= len(self.encrypted_text):
                self.cursor_position = 0
            else:
                self.cursor_position += 1
            while not self.encrypted_text[self.cursor_position].isalpha():
                if self.cursor_position + 1 >= len(self.encrypted_text):
                    self.cursor_position = 0
                else:
                    self.cursor_position += 1

        # Check if the game is complete after each update

    def shift_guess(self, direction):
        """Shift the guessed letter for an encrypted letter up/down the alphabet."""
        encrypted_letter = self.encrypted_text[self.cursor_position]

        if not encrypted_letter.isalpha():
            return

        alphabet = string.ascii_uppercase
        current_guess = self.letter_mappings.get(encrypted_letter, "_")

        if current_guess == "_":
            new_guess = "A" if direction == 1 else "Z"
        elif current_guess == "A" and direction == -1:
            new_guess = "_"
        elif current_guess == "Z" and direction == 1:
            new_guess = "_"
        else:
            idx = alphabet.index(current_guess)
            new_idx = (idx + direction) % 26
            new_guess = alphabet[new_idx]

        self.letter_mappings[encrypted_letter] = new_guess  # Update mapping

    def is_puzzle_solved(self):
        return self.apply_guess() == self.decrypted_text

    def apply_guess(self):
        """Return the decrypted puzzle using current letter mappings."""
        return "".join(self.letter_mappings.get(char, "_") if char.isalpha() else char for char in self.encrypted_text)

    def start_over(self, default_mapping = {}):
        self.letter_mappings = default_mapping.copy()
        self.reset_cursor_position()

    def show_hint(self):
        """Provide a hint by setting one incorrect/blank guess to the correct letter."""
        # Find all incorrect/blank guesses
        incorrect_guesses = [k for k, v in self.key.items()
                             if (v not in self.letter_mappings or self.letter_mappings[v] != k) and
                                 k in self.decrypted_text]

        if incorrect_guesses:
            # Randomly select an incorrect guess
            selected_char = random.choice(incorrect_guesses)
            # Get the correct letter for the selected encrypted letter
            correct_guess = self.key[selected_char]

            # Set the guessed letter to the correct one
            self.letter_mappings[correct_guess] = selected_char

    def get_red_marks(self):
        """Detect conflicts where multiple encrypted letters map to the same guessed letter."""
        reverse_mapping = {}  # Maps guessed letters to encrypted letters
        red_letters = set()   # Set of guessed letters that cause conflicts

        for encrypted_letter, guessed_letter in self.letter_mappings.items():
            if not encrypted_letter.isalpha() or guessed_letter == '_':
                continue
            elif guessed_letter in reverse_mapping:
                red_letters.add(guessed_letter)  # Mark as conflicting
            else:
                reverse_mapping[guessed_letter] = encrypted_letter

        # Generate red marks based on which letters are invalid
        return "".join("r" if self.letter_mappings.get(char, "_") in red_letters else " "
                       for char in self.encrypted_text)
